/*
 * Copyright 2024 The CHANGLEI Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import 'dart:math' as math;
import 'dart:ui';

import 'package:flatterer/src/graphical.dart' as graphical;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';

/// Linearly interpolate between two doubles.
///
/// Same as [lerpDouble] but specialized for non-null `double` type.
double _lerpDouble(double a, double b, double t) {
  return a * (1.0 - t) + b * t;
}

/// 圆角大小
final class RSize {
  /// 圆角大小
  const RSize(
    this.width,
    this.height, {
    this.radius = 0,
    this.blRadius = 0,
    this.brRadius = 0,
  })  : assert(radius >= 0),
        assert(blRadius >= 0),
        assert(brRadius >= 0);

  /// An empty size, one with a zero width and a zero height.
  static const zero = RSize(0, 0);

  /// 宽
  final double width;

  /// 高
  final double height;

  /// 顶部的圆角
  final double radius;

  /// 左下的圆角
  final double blRadius;

  /// 右下的圆角
  final double brRadius;

  /// lerp
  static RSize? lerp(RSize? a, RSize? b, double t) {
    if (b == null) {
      if (a == null) {
        return null;
      } else {
        return a * (1.0 - t);
      }
    } else {
      if (a == null) {
        return b * t;
      } else {
        return RSize(
          _lerpDouble(a.width, b.width, t),
          _lerpDouble(a.height, b.height, t),
          radius: _lerpDouble(a.radius, b.radius, t),
          blRadius: _lerpDouble(a.blRadius, b.blRadius, t),
          brRadius: _lerpDouble(a.brRadius, b.brRadius, t),
        );
      }
    }
  }

  /// *
  RSize operator *(double t) {
    return RSize(
      width * t,
      height * t,
      radius: radius * t,
      blRadius: blRadius * t,
      brRadius: brRadius * t,
    );
  }

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is RSize &&
          runtimeType == other.runtimeType &&
          width == other.width &&
          height == other.height &&
          radius == other.radius &&
          blRadius == other.blRadius &&
          brRadius == other.brRadius;

  @override
  int get hashCode => width.hashCode ^ height.hashCode ^ radius.hashCode ^ blRadius.hashCode ^ brRadius.hashCode;
}

/// Created by box on 2020/7/28.
///
/// 带有指示器的自定义border
class IndicateBorder extends OutlinedBorder {
  /// 带有指示器的自定义border
  const IndicateBorder({
    RSize? indicateSize,
    BorderRadiusGeometry? borderRadius,
    Rect? anchor,
    this.direction = Axis.vertical,
    BorderSide side = BorderSide.none,
  })  : indicateSize = indicateSize ?? RSize.zero,
        borderRadius = borderRadius ?? BorderRadius.zero,
        anchor = anchor ?? Rect.zero,
        super(side: side);

  /// 三角形的大小，宽高对应三角形的底边和高
  final RSize indicateSize;

  /// 矩形框的圆角
  final BorderRadiusGeometry borderRadius;

  /// 锚点的位置大小
  final Rect anchor;

  /// 绘制的方向
  final Axis direction;

  @override
  ShapeBorder scale(double t) {
    return IndicateBorder(
      side: side.scale(t),
      borderRadius: borderRadius * t,
      indicateSize: indicateSize * t,
      direction: direction,
      anchor: Rect.fromPoints(
        anchor.topLeft * t,
        anchor.bottomRight * t,
      ),
    );
  }

  @override
  ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
    if (a is IndicateBorder) {
      return IndicateBorder(
        side: BorderSide.lerp(a.side, side, t),
        borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t),
        indicateSize: RSize.lerp(a.indicateSize, indicateSize, t),
        anchor: Rect.lerp(a.anchor, anchor, t),
        direction: direction,
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
  ShapeBorder? lerpTo(ShapeBorder? b, double t) {
    if (b is IndicateBorder) {
      return IndicateBorder(
        side: BorderSide.lerp(side, b.side, t),
        borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t),
        indicateSize: RSize.lerp(indicateSize, b.indicateSize, t),
        anchor: Rect.lerp(anchor, b.anchor, t),
        direction: direction,
      );
    }
    return super.lerpTo(b, t);
  }

  @override
  IndicateBorder copyWith({
    RSize? indicateSize,
    BorderRadius? borderRadius,
    Rect? anchor,
    Axis? direction,
    BorderSide? side,
  }) {
    return IndicateBorder(
      indicateSize: indicateSize ?? this.indicateSize,
      borderRadius: borderRadius ?? this.borderRadius,
      side: side ?? this.side,
      direction: direction ?? this.direction,
      anchor: anchor ?? this.anchor,
    );
  }

  @override
  Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
    return _getPath(rect, textDirection: textDirection, delta: side.strokeInset);
  }

  @override
  Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
    return _getPath(rect, textDirection: textDirection);
  }

  Path _getIndicatePath(
    double width,
    double height,
    double radius,
    double blRadius,
    double brRadius, [
    AxisDirection? direction,
    Offset? offset,
  ]) {
    Path path = graphical.trianglePath(
      width: width,
      height: height,
      radius: radius,
      blRadius: blRadius,
      brRadius: brRadius,
      avoidOffset: true,
    );
    path = path.shift(Offset(-width / 2, 0));
    if (direction != null && direction.angle % 360 != 0) {
      path = path.transform(Matrix4.rotationZ(direction.radians).storage);
    }
    if (offset != null) {
      path = path.shift(offset);
    }
    return path;
  }

  Path _getPath(Rect rect, {TextDirection? textDirection, double delta = 0}) {
    final indicateWidth = indicateSize.width;
    final indicateHeight = indicateSize.height;

    double newIndicateWidth;
    double newIndicateHeight;
    double newDelta;
    if (indicateHeight == 0) {
      newIndicateWidth = indicateWidth;
      newIndicateHeight = indicateHeight;
      newDelta = 0;
    } else {
      final radians = math.atan(indicateWidth / 2 / indicateHeight);
      newDelta = delta / math.sin(radians);
      newIndicateHeight = indicateHeight - newDelta + delta;
      newIndicateWidth = newIndicateHeight * math.tan(radians) * 2;
    }

    final indicateOriginSize = indicateHeight;

    AxisDirection? axisDirection;
    Offset offset;
    if (direction == Axis.vertical) {
      final translation = (anchor.center - rect.centerLeft).dx;
      if (rect.center.dy < anchor.center.dy) {
        axisDirection = AxisDirection.down;
        offset = Offset(translation, rect.height - newDelta + indicateOriginSize);
      } else {
        axisDirection = AxisDirection.up;
        offset = Offset(translation, newDelta - indicateOriginSize);
      }
    } else {
      final translation = (anchor.center - rect.topCenter).dy;
      if (rect.center.dx < anchor.center.dx) {
        axisDirection = AxisDirection.right;
        offset = Offset(rect.width - newDelta + indicateOriginSize, translation);
      } else {
        axisDirection = AxisDirection.left;
        offset = Offset(newDelta - indicateOriginSize, translation);
      }
    }

    return Path.combine(
      PathOperation.union,
      _getIndicatePath(
        newIndicateWidth,
        newIndicateHeight,
        indicateSize.radius,
        indicateSize.blRadius,
        indicateSize.brRadius,
        axisDirection,
        offset + rect.topLeft,
      ),
      Path()..addRRect(borderRadius.resolve(textDirection).toRRect(rect).deflate(delta)),
    );
  }

  @override
  void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
    Path getPath([double delta = 0.0]) {
      return _getPath(rect, textDirection: textDirection, delta: delta);
    }

    switch (side.style) {
      case BorderStyle.none:
        break;
      case BorderStyle.solid:
        final width = side.width;
        if (width == 0) {
          canvas.drawPath(getPath(), side.toPaint());
          return;
        }
        final path = Path()
          ..addPath(getPath(side.strokeOutset), Offset.zero)
          ..addPath(getPath(side.strokeInset), Offset.zero)
          ..fillType = PathFillType.evenOdd
          ..close();
        final paint = side.toPaint()..style = PaintingStyle.fill;
        canvas.drawPath(path, paint);
    }
  }

  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is IndicateBorder &&
        other.side == side &&
        other.borderRadius == borderRadius &&
        other.indicateSize == indicateSize &&
        other.anchor == anchor &&
        other.direction == direction;
  }

  @override
  int get hashCode => Object.hash(side, borderRadius, indicateSize, anchor, direction);

  @override
  String toString() {
    return '${objectRuntimeType(this, 'IndicateBorder')}($indicateSize, $borderRadius, $anchor, $direction, $side)';
  }
}

extension _AxisDirectionExtension on AxisDirection {
  /// 对应的角度
  double get angle {
    switch (this) {
      case AxisDirection.up:
        return 0;
      case AxisDirection.right:
        return 90;
      case AxisDirection.down:
        return 180;
      case AxisDirection.left:
        return 270;
    }
  }

  /// 弧度
  double get radians => angle * math.pi / 180;
}
